﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Sustainalytics.ClientDW.Synchronization.RiskRatingsBenchmarks
{
    public class ComparativeGroup<T, TK, TU>
    {
        readonly Dictionary<TK, int> _directIndex = new Dictionary<TK, int>();
        readonly Dictionary<TK, T> _reverseIndex = new Dictionary<TK, T>();

        readonly Dictionary<int, int> groupCount = new Dictionary<int, int>();
        readonly Dictionary<TU, int> groupMapping = new Dictionary<TU, int>();
        readonly Dictionary<int, double> groupAverage = new Dictionary<int, double>();
        readonly Dictionary<int, CompanyScore<TK>> groupMin = new Dictionary<int, CompanyScore<TK>>();
        readonly Dictionary<int, CompanyScore<TK>> groupMax = new Dictionary<int, CompanyScore<TK>>();

        readonly Dictionary<TK, int> rank = new Dictionary<TK, int>();

        internal Func<T, double?> ScoreSelector { get; set; }
        internal Func<T, TK> KeySelector { get; set; }

        Func<T, string> _nameSelector;

        public int Count { get; set; }

        public List<T> Companies => _reverseIndex.Values.ToList();

        public T Company(TK key) => _reverseIndex[key];


        IComparer<double?> scoreComparer;

        ComparativeGroup()
        { }

        public static ComparativeGroup<T, TK, TU> Create(ICollection<T> collection, Func<T, TK> keySelector, Func<T, string> nameSelector, Func<T, TU> sortKeySelector, IComparer<TU> sortkeyComparer, Func<T, double?> scoreSelector, IComparer<double?> scoreComparer)
        {
            int currentIndex = 0;
            var returnIndex = new ComparativeGroup<T, TK, TU>();
            T previousItem = default(T);
            returnIndex._nameSelector = nameSelector;
            bool first = true;

            returnIndex.KeySelector = keySelector;
            returnIndex.ScoreSelector = scoreSelector;
            returnIndex.scoreComparer = scoreComparer;

            int count = 0;
            double sum = 0;
            int rank = 1;
            CompanyScore<TK> min = new CompanyScore<TK> { Score = Double.MaxValue };
            CompanyScore<TK> max = new CompanyScore<TK> { Score = Double.MinValue };
            int itemsWithSameRank = 1;
            int generalCount = 0;


            IOrderedEnumerable<T> orderedData;


            if (sortKeySelector != null && sortkeyComparer != null)
            {
                orderedData = collection.OrderBy(sortKeySelector, sortkeyComparer).ThenBy(scoreSelector, scoreComparer);
            }
            else
            {
                orderedData = collection.OrderBy(scoreSelector, scoreComparer);
            }

            foreach (var item in orderedData)
            {
                generalCount++;
                var projectedItem = keySelector(item);
                returnIndex._reverseIndex.Add(projectedItem, item);
                if (!first)
                {
                    if (!ItemsInSameComparativeGroup(sortKeySelector, sortkeyComparer, previousItem, item))
                    {
                        returnIndex.groupCount.Add(currentIndex, count);
                        returnIndex.groupAverage.Add(currentIndex, sum / count);
                        returnIndex.groupMax.Add(currentIndex, max);
                        returnIndex.groupMin.Add(currentIndex, min);
                        returnIndex.groupMapping.Add(sortKeySelector(previousItem), currentIndex);
                        count = 0;
                        sum = 0;
                        rank = 1;
                        itemsWithSameRank = 1;
                        min = new CompanyScore<TK> { Score = Double.MaxValue };
                        max = new CompanyScore<TK> { Score = Double.MinValue };
                        currentIndex++;
                    }

                    if (ItemsInSameComparativeGroup(sortKeySelector, sortkeyComparer, previousItem, item))
                    {
                        if ((scoreComparer.Compare(scoreSelector(item), scoreSelector(previousItem)) == 0))
                        {
                            itemsWithSameRank++;
                        }
                        else
                        {
                            rank += itemsWithSameRank;
                            itemsWithSameRank = 1;
                        }
                    }
                }

                first = false;

                if (scoreComparer.Compare(min.Score, scoreSelector(item)) > 0)
                {
                    min = new CompanyScore<TK> { Score = scoreSelector(item), Id = projectedItem };
                }

                if (scoreComparer.Compare(max.Score, scoreSelector(item)) < 0)
                {
                    max = new CompanyScore<TK> { Score = scoreSelector(item), Id = projectedItem };
                }

                
                sum += scoreSelector(item).GetValueOrDefault();


                count++;
                returnIndex._directIndex.Add(projectedItem, currentIndex);
                returnIndex.rank.Add(projectedItem, rank);
                previousItem = item;
            }

            if (sortKeySelector != null)
            {
                returnIndex.groupMapping.Add(sortKeySelector(previousItem), currentIndex);
            }
            returnIndex.groupCount.Add(currentIndex, count);
            returnIndex.groupAverage.Add(currentIndex, sum / count);
            returnIndex.groupMax.Add(currentIndex, max);
            returnIndex.groupMin.Add(currentIndex, min);
            returnIndex.Count = generalCount;

            return returnIndex;
        }


        public int GetGroupCount(TU groupKey)
        {
            var groupIndex = groupMapping[groupKey];
            return groupCount[groupIndex];
        }

        T GetTopPerformer(int groupId)
        {
            var companyKey = groupMax[groupId].Id;
            return _reverseIndex[companyKey];
        }

        T GetBottomPerformer(int groupId)
        {
            var companyKey = groupMin[groupId].Id;
            return _reverseIndex[companyKey];
        }

        static bool ItemsInSameComparativeGroup(Func<T, TU> sortKeySelector, IComparer<TU> sortkeyComparer, T previousItem, T item)
        {
            if (sortKeySelector == null && sortkeyComparer == null)
            {
                return true;
            }
            return sortkeyComparer.Compare(sortKeySelector(previousItem), sortKeySelector(item)) == 0;
        }

        public int GetGroupIndex(TK keySelector)
        {
            if (_directIndex.TryGetValue(keySelector, out int index))
            {
                return index;
            }

            return -1;
        }

        public int GetComparativeGroupCount(int groupIndex)
        {
            return groupCount[groupIndex];
        }

        public string GetCompanyName(TK key)
        {
            var item = _reverseIndex[key];
            if (!item.Equals(default(T)))
            {
                return _nameSelector(item);
            }
            return string.Empty;
        }

        public double? GetScore(TK keySelector)
        {
            if (_reverseIndex.TryGetValue(keySelector, out T index))
            {
                return ScoreSelector(index);
            }
            throw new IndexOutOfRangeException(keySelector.ToString());
        }

        public Benchmark<T> GetBenchmark(TK keySelector)
        {
            var groupIndex = GetGroupIndex(keySelector);
            if (groupIndex >= 0)
            {
                return new Benchmark<T>
                {
                    Rank = rank[keySelector],
                    Count = groupCount[groupIndex],
                    Percentile = ((((double)rank[keySelector] - 1) / (groupCount[groupIndex] - 1)) * 0.99) + 0.01,
                    Average = groupAverage[groupIndex],
                    Max = GetTopPerformer(groupIndex),
                    Min = GetBottomPerformer(groupIndex)
                };
            }
            throw new IndexOutOfRangeException(keySelector.ToString());
        }

        public int GetComparativeGroupsCount() => groupMapping.Count();

        public IEnumerable<TU> GetComparativeGroups() => groupMapping.Keys;


        int GetBinByScore(double? score)
        {

            if (scoreComparer.Compare(score, 0) == -1)
            { throw new IndexOutOfRangeException(); }

            if (scoreComparer.Compare(score, 100) == 1)
            { throw new IndexOutOfRangeException(); }


            return (int)Math.Floor(score.GetValueOrDefault());

        }
    }
}